package com.example.sendmessage.server.dao;

import com.example.sendmessage.common.BinaryTool;
import com.example.sendmessage.common.MqException;
import com.example.sendmessage.server.core.Message;
import com.example.sendmessage.server.core.Queue;

import java.io.*;
import java.util.LinkedList;
import java.util.Scanner;

/**
 * 消息的持久化管理
 */
public class MessageFileManager {
    // 用于统计消息的内部类
    public static class Stat{
        public int totalCount = 0; // 总的消息文件数量
        public int validCount = 0; // 未被删除的消息文件数量
    }

    /**
     * 消息持久化的初始化过程
     */
    public void init() {}

    /**
     * 获取当前队列的消息持久化目录
     * @param queueName 队列的名字
     * @return 队列的持久化目录
     */
    private String getQueueDir(String queueName) {
        return "./data/" + queueName;
    }

    /**
     * 获取当前队列的消息数据文件路径
     * @param queueName 队列名字
     * @return 消息数据文件路径
     */
    private String getQueueDataPath(String queueName) {
        return getQueueDir(queueName) + "/queue_data.txt";
    }

    /**
     * 获取当前队列的消息统计文件路径
     * @param queueName 队列名字
     * @return 消息统计文件路径
     */
    private String getQueueStatPath(String queueName) {
        return getQueueDir(queueName) + "/queue_stat.txt";
    }

    /**
     * 创建消息持久化对应的目录和文件
     * @param queueName 队列名字
     * @throws MqException 目录创建失败的异常
     * @throws IOException 文件创建失败的异常
     */
    public void createQueueFiles(String queueName) throws MqException, IOException {
        // 创建消息文件的目录
        File queueDir = new File(getQueueDir(queueName));
        if (!queueDir.exists()) {
            boolean ok = queueDir.mkdirs();
            if (ok) {
                System.out.println("[MessageFileManager] 消息文件目录创建成功！queueDir = " + queueDir.getAbsolutePath());
            } else {
                throw new MqException("[MessageFileManager] 消息文件目录创建失败！queueDir = " + queueDir.getAbsolutePath());
            }
        }
        // 创建消息数据文件
        File queueDataFile = new File(getQueueDataPath(queueName));
        if (!queueDataFile.exists()) {
            boolean ok = queueDataFile.createNewFile();
            if (ok) {
                System.out.println("[MessageFileManager] 消息数据文件创建成功！queueDataFile = " + queueDataFile.getAbsolutePath());
            } else {
                throw new IOException("[MessageFileManager] 消息数据文件创建失败！queueDataFile = " + queueDataFile.getAbsolutePath());
            }
        }
        // 创建消息统计文件
        File queueStatFile = new File(getQueueStatPath(queueName));
        if (!queueStatFile.exists()) {
            boolean ok = queueStatFile.createNewFile();
            if (ok) {
                System.out.println("[MessageFileManager] 消息统计文件创建成功！queueStatFile = " + queueStatFile.getAbsolutePath());
            } else {
                throw new IOException("[MessageFileManager] 消息统计文件创建失败！queueStatFile = " + queueStatFile.getAbsolutePath());
            }
        }
        // 初始化统计文件的数据
        Stat stat = new Stat();
        stat.totalCount = 0;
        stat.validCount = 0;
        // 将统计写入文件中
        writeStat(queueName, stat);
    }

    /**
     * 删除消息持久化文件
     * @param queueName 队列名
     * @throws MqException 删除文件失败异常
     */
    public void deleteQueueFiles(String queueName) throws MqException {
        // 先删除文件在删除目录
        File queueDataFile = new File(getQueueDataPath(queueName));
        boolean ok1 = queueDataFile.delete();
        File queueStatFile = new File(getQueueStatPath(queueName));
        boolean ok2 = queueStatFile.delete();
        // 删除目录
        File queueDir = new File(getQueueDir(queueName));
        boolean ok3 = queueDir.delete();
        if (!ok1 || !ok2 || !ok3) {
            throw new MqException("[MessageFileManager] 消息持久化文件删除失败！queueDir = " + queueDir.getAbsolutePath());
        } else {
            System.out.println("[MessageFileManager] 消息持久化文件删除成功！queueDir = " + queueDir.getAbsolutePath());
        }
    }

    /**
     * 将统计结果写入统计文件中
     * @param queueName 队列名
     * @param stat 统计类
     * @throws IOException 写入文件时异常
     */
    private void writeStat(String queueName, Stat stat){
        try (OutputStream outputStream = new FileOutputStream(getQueueStatPath(queueName))) {
            PrintWriter printWriter = new PrintWriter(outputStream);
            printWriter.write(stat.totalCount + "\t" + stat.validCount);
            printWriter.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 读取统计文件数据，用于判断是否gc
     * @param queueName 队列名
     * @return 统计结果类
     */
    private Stat readStat(String queueName) {
        Stat stat = new Stat();
        try (InputStream inputStream = new FileInputStream(getQueueStatPath(queueName))) {
            Scanner scanner = new Scanner(inputStream);
            stat.totalCount = scanner.nextInt();
            stat.validCount = scanner.nextInt();
            return stat;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 判断队列文件是否存在
     * @param queueName 队列名字
     * @return 是否存在
     */
    private boolean checkFileExists(String queueName) {
        File dataFile = new File(getQueueDataPath(queueName));
        if (!dataFile.exists()) {
            return false;
        }
        File statFile = new File(getQueueStatPath(queueName));
        return statFile.exists();
    }

    /**
     * 将消息写入文件进行持久化存储
     * @param queue 需要消息持久化的队列队列
     * @param message 需要持久化的消息
     * @throws MqException 文件是否存在
     * @throws IOException 写入文件流的异常
     */
    public void sendMessage(Queue queue, Message message) throws MqException, IOException {
        // 判断两个文件是否存在
        if (!checkFileExists(queue.getName())) {
            throw new MqException("[MessageFileManager] 队列对应文件不存在！queueName = " + queue.getName());
        }
        // 将消息序列化成字节数组
        byte[] messageBinary = BinaryTool.toBytes(message);
        synchronized (queue) {
            File dataFile = new File(getQueueDataPath(queue.getName()));
            // 设置消息的起始位置，跳过的4字节是消息的长度
            message.setOffsetBeg(dataFile.length() + 4);
            message.setOffsetEnd(dataFile.length() + 4 + messageBinary.length);
            // 将消息写入文件
            try (OutputStream outputStream = new FileOutputStream(dataFile, true)) {
                try (DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
                    // 存入消息的长度，方便后面读取
                    dataOutputStream.writeInt(messageBinary.length);
                    // 存入消息
                    dataOutputStream.write(messageBinary);
                }
            }
            // 更新统计文件
            Stat stat = readStat(queue.getName());
            stat.totalCount += 1;
            stat.validCount += 1;
            writeStat(queue.getName(), stat);
        }
    }

    /**
     * 删除持久化的消息，更改状态的方式
     * @param queue 队列
     * @param message 消息
     * @throws IOException 随机读取文件异常
     * @throws ClassNotFoundException 序列化异常
     */
    public void deleteMessage(Queue queue, Message message) throws IOException, ClassNotFoundException {
        synchronized (queue) {
            try (RandomAccessFile randomAccessFile = new RandomAccessFile(getQueueDataPath(queue.getName()), "rw")) {
                // 获取消息文件长度，用字节数组来接收
                byte[] body = new byte[(int) (message.getOffsetEnd() - message.getOffsetBeg())];
                // 文件定位到消息开始的地方，准备读取
                randomAccessFile.seek(message.getOffsetBeg());
                randomAccessFile.read(body);
                // 将消息字节数组反序列化成对象
                Message diskMessage = (Message) BinaryTool.fromBytes(body);
                diskMessage.setIsValid((byte) 0x0);
                // 修改存在状态后在将新的消息写入文件
                byte[] newBody = BinaryTool.toBytes(diskMessage);
                // 定位到开始位置，并重新写入
                randomAccessFile.seek(message.getOffsetBeg());
                randomAccessFile.write(newBody);
                // 更新统计文件的数据
                Stat stat = readStat(queue.getName());
                if (stat != null && stat.validCount > 0) {
                    stat.validCount -= 1;
                    writeStat(queue.getName(), stat);
                }
            }
        }
    }

    /**
     * 消息可靠性，保证宕机也能将数据重新加载
     * @param queueName 队列名
     * @return 消息链表，存放有效地消息
     * @throws IOException 文件流异常
     * @throws MqException 文件读取时格式不匹配
     * @throws ClassNotFoundException 序列化异常
     */
    public LinkedList<Message> loadAllMessagesFromQueue(String queueName) throws IOException, MqException, ClassNotFoundException {
        LinkedList<Message> messages = new LinkedList<>();
        try (InputStream inputStream = new FileInputStream(getQueueDataPath(queueName))) {
            try(DataInputStream dataInputStream = new DataInputStream(inputStream)) {
                int currentOffset = 0;
                while (true) {
                    // 先读取消息的长度，根据长度读取消息内容
                    int messageSize = dataInputStream.readInt();
                    byte[] buffer = new byte[messageSize];
                    int readSize = dataInputStream.read(buffer);
                    if (readSize != messageSize) {
                        throw new MqException("[MessageFileManager] 文件读取格式错误！queueName = " + queueName);
                    }
                    // 序列化成消息对象
                    Message message = (Message) BinaryTool.fromBytes(buffer);
                    // 判断消息是否有效，无效的消息就不加载了
                    if (message.getIsValid() != 0x1) {
                        // 无效消息也需要跳过
                        currentOffset += (4 + messageSize);
                        continue;
                    }
                    // 设置消息在文件的始末位置
                    message.setOffsetBeg(currentOffset + 4);
                    message.setOffsetEnd(currentOffset + 4 + messageSize);
                    // 指向下一个消息，4是消息的长度要跳过
                    currentOffset += (4 + messageSize);
                    messages.add(message);
                }
            } catch (EOFException e) {
                // 流无法结束，用异常来结束
                System.out.println("[MessageFileManager] 消息数据恢复成功！");
            }
        }
        return messages;
    }

    /**
     * 判断是否需要将垃圾消息进行回收
     * 总量超过 2000 且 垃圾消息超过一半就回收
     * @param queueName 待判断垃圾回收的队列名
     * @return 是否回收
     */
    public boolean checkGC(String queueName) {
        Stat stat = readStat(queueName);
        return stat.totalCount > 2000 && (double)stat.validCount / (double)stat.totalCount < 0.5;
    }

    /**
     * 创建一个GC文件，将有用的消息先写入这个文件
     * 最后将gc文件替换原文件达到gc效果
     * @param queueName gc的队列名
     * @return gc的文件
     */
    private String createGCFile(String queueName) {
        return getQueueDir(queueName) + "/queue_data_new.txt";
    }

    /**
     * 垃圾回收的过程
     * @param queue 需要gc的队列
     * @throws MqException 创建、删除文件的异常
     * @throws IOException 文件写入异常
     * @throws ClassNotFoundException 序列化异常
     */
    public void gc(Queue queue) throws MqException, IOException, ClassNotFoundException {
        synchronized (queue) {
            // 记录一下整个gc的时间
            long gcBeg = System.currentTimeMillis();
            // 创建一个新的文件用于gc，最后替换原文件达到gc的效果
            File gcDataFile = new File(createGCFile(queue.getName()));
            if (gcDataFile.exists()) {
                // gc时是替换的过程，所以gc文件不应该存在
                throw new MqException("[MessageFileManager] gc时数据文件存在，上次的gc失败！queueName = " + queue.getName());
            }
            boolean ok = gcDataFile.createNewFile();
            if (!ok) {
                throw new MqException("[MessageFileManager] 创建gc文件失败！gcDataFile = " + gcDataFile.getAbsolutePath());
            }
            // 将所有的有效消息写入新的文件中
            LinkedList<Message> messages = loadAllMessagesFromQueue(queue.getName());
            try (OutputStream outputStream = new FileOutputStream(gcDataFile, true)) {
                try (DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
                    for (Message message : messages) {
                        byte[] buffer = BinaryTool.toBytes(message);
                        dataOutputStream.writeInt(buffer.length);
                        dataOutputStream.write(buffer);
                    }
                }
            }
            // 删除原来的消息数据文件
            File dataFile = new File(getQueueDataPath(queue.getName()));
            ok = dataFile.delete();
            if (!ok) {
                throw new MqException("[MessageFileManager] gc时删除原文件失败！dataFile = " + dataFile.getAbsolutePath());
            }
            // 将gc文件重命名为原文件名
            ok = gcDataFile.renameTo(dataFile);
            if (!ok) {
                throw new MqException("[MessageFileManager] gc时替换原文件失败！gcDataFile = " + gcDataFile.getAbsolutePath());
            }
            // 修改统计文件数据
            Stat stat = new Stat();
            stat.totalCount = messages.size();
            stat.validCount = messages.size();
            writeStat(queue.getName(), stat);
            // 计算整个gc所消耗的时间
            long gcEnd = System.currentTimeMillis();
            System.out.println("[MessageFileManager] gc完毕！queueName = " + queue.getName()+
                    "消耗的时间 = " + (gcEnd - gcBeg)+"ms");
        }
    }
}
